﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Configuration;
using System.Xml;
using System.Xml.Schema;
using System.Reflection;
using Microsoft.Win32;
using AddIn.EA.Core.Common;

namespace AddIn.EA.Util.AddInActions
{
    public class AddInInstall
    {
        #region Delegates
        public delegate void AddInInstallMessage(object sender, AddInInstallMessageEventArgs e);
        #endregion

        #region Classes
        private class AddIn
        {
            #region Classes
            public interface IGenerateXmlElement
            {
                XmlElement GenerateXmlElement(XmlDocument xml);
            }

            public class Menu : IGenerateXmlElement
            {
                public Menu() { }
                public Menu(string name) : this(name, null) { }
                public Menu(string name, Items items)
                {
                    this.Name = name;
                    this.Items = items;
                }

                public string Name { get; set; }
                public Items Items { get; set; }

                #region IGenerateXmlElement Members

                public XmlElement GenerateXmlElement(XmlDocument xml)
                {
                    XmlElement menu = xml.CreateElement("Menu");

                    XmlElement name = xml.CreateElement("Name");
                    name.InnerText = this.Name;

                    menu.AppendChild(name);

                    if (Items != null)
                        menu.AppendChild(Items.GenerateXmlElement(xml));

                    return menu;
                }

                #endregion


            }

            public class Items : List<Item>, IGenerateXmlElement
            {
                #region IGenerateXml Members

                public XmlElement GenerateXmlElement(XmlDocument xml)
                {
                    XmlElement items = xml.CreateElement("Items");

                    foreach (Item item in this)
                        items.AppendChild(item.GenerateXmlElement(xml));

                    return items;
                }

                #endregion
            }

            public class Item : IGenerateXmlElement
            {
                public Item(string itemName, string assembly)
                {
                    this.ItemName = itemName;
                    this.Assembly = assembly;
                }

                public string ItemName { get; set; }
                public string Assembly { get; set; }

                #region IGenerateXml Members

                public XmlElement GenerateXmlElement(XmlDocument xml)
                {
                    XmlElement item = xml.CreateElement("Item");

                    XmlAttribute itemName = xml.CreateAttribute("itemName");
                    itemName.Value = this.ItemName;

                    item.Attributes.Append(itemName);

                    XmlAttribute assembly = xml.CreateAttribute("assembly");
                    assembly.Value = this.Assembly != null ? this.Assembly : string.Empty;

                    item.Attributes.Append(assembly);

                    return item;
                }

                #endregion
            }
            #endregion

            #region Fields
            private bool _IsValidXml = true;
            #endregion
            
            #region Properties
            public IGenerateXmlElement[] Body { get; set; }
            public string XmlFileName { get; set; }
            public string Result { get; set; }
            #endregion

            #region Methods
            public bool GenerateXml()
            {
                try
                {
                    XmlDocument xml = new XmlDocument();

                    XmlDeclaration declaration = xml.CreateXmlDeclaration("1.0", "utf-8", null);
                    xml.AppendChild(declaration);

                    XmlElement root = xml.CreateElement("AddIn");

                    XmlAttribute attribNamespace = xml.CreateAttribute("xmlns");
                    attribNamespace.Value = "http://bovespa/schemas/AddInArchitecture.xsd";

                    root.Attributes.Append(attribNamespace);
                    foreach(IGenerateXmlElement bodyItem in this.Body)
                        root.AppendChild(bodyItem.GenerateXmlElement(xml));

                    xml.AppendChild(root);

                    xml.Schemas.Add(GetSchema());

                    if (_IsValidXml)
                        xml.Validate(new ValidationEventHandler(XmlValidation));

                    if (!_IsValidXml)
                    {
                        Result = "Xml Inválido!";
                        return false;
                    }
                    else
                    {
                        try
                        {
                            string menuFolder = Path.Combine(AddInRegistryResources.GetMenuFolder(), AddInRegistryResources.GetMenuName());
                            xml.Save(Path.Combine(menuFolder, this.XmlFileName));
                        }
                        catch (Exception ex)
                        {
                            Result = string.Format("Ocorreu um erro ao salvar o XML do menu.\n\n{0}\n\n{1}", ex.Message, ex.StackTrace);
                            return false;
                        }
                    }

                    return true;

                }
                catch(Exception ex)
                {
                    this.Result = string.Format("Ocorreram erros ao salvar o XML do menu.\n\n{0}\n\n{1}", ex.Message, ex.StackTrace);
                    return false;
                }
            }

            

            private XmlSchema GetSchema()
            {
                XmlSchema schema = null;

                using (StreamReader reader = new StreamReader(AddInRegistryResources.GetSchemaFile(), UTF32Encoding.UTF8))
                {
                    schema = XmlSchema.Read(reader, new ValidationEventHandler(XmlValidation));
                    reader.Close();
                }

                return schema;
            }

            void XmlValidation(object sender, ValidationEventArgs e)
            {
                _IsValidXml = false;
            }
            #endregion 

        }

        public class AddInInstallMessageEventArgs : EventArgs
        {
            public AddInInstallMessageEventArgs(string message)
            {
                this.Message = message;
            }
            public string Message { get; private set; }
        }
        #endregion

        #region Events
        public event AddInInstallMessage OnSendMessage;
        #endregion

        #region Methods
        public void Install(string assemblyFullPath)
        {
            try
            {
                SendMessage(string.Format("\n{0}\n\n", Properties.Resources.AddInArchitectureHeader));
                SendMessage("INSTALL\n\n");
                SendMessage(string.Format("Process to install the assembly {0} was started!", assemblyFullPath));
                SendMessage("Importing assembly file...");

                if (!File.Exists(assemblyFullPath))
                {
                    SendMessage(string.Format("Cannot find the file {0}", assemblyFullPath));
                    return;
                }

                FileInfo assemblyFile = CopyToAddInFolder(assemblyFullPath);

                SendMessage("Loading assembly file...");

                AddIn addIn = ReadAssembly(assemblyFile);

                SendMessage("Saving settings...");

                bool installed = true;
                if (addIn.Body != null && addIn.Body.Length > 0)
                {
                    if (!addIn.GenerateXml())
                    {
                        SendMessage(addIn.Result);
                        installed = false;
                    }
                }
                else
                {
                    SendMessage("There aren't types that implement IAddInArchitecture or they aren't decorated with AddInMenuAttribute.");
                    installed = false;
                }

                if(installed)
                    SendMessage("Assembly installed successfully!");
            }
            catch(Exception ex)
            {
                SendMessage(string.Format("An exception has been occurred during the instalation.\n\n{0}:\n{1}", ex.Message, ex.StackTrace));
            }
        }

        public void Uninstall(string assemblyName)
        {
            try
            {
                SendMessage(string.Format("\n{0}\n\n", Properties.Resources.AddInArchitectureHeader));
                SendMessage("UNINSTALL\n\n");
                SendMessage(string.Format("Process to uninstall the assembly {0} was started!", assemblyName));

                string assemblyFile = Path.Combine(AddInRegistryResources.GetApplicationFolder(), assemblyName);
                if (!File.Exists(assemblyFile))
                {
                    SendMessage("Assembly was not found!");
                    return;
                }

                SendMessage("Removing assembly...");

                RemoveAssembly(assemblyFile);

                SendMessage("Removing settings...");

                string assemblyXmlFile = Path.Combine(AddInRegistryResources.GetMenuFolder(), Path.Combine(AddInRegistryResources.GetMenuName(), string.Format("{0}.xml", assemblyName)));
                File.Delete(assemblyXmlFile);

                SendMessage("Assembly uninstalled successfully!");
            }
            catch (UnauthorizedAccessException)
            {
                SendMessage(string.Format("ERROR! The assembly {0} has been using by Enterprise Architect!", assemblyName));
            }
            catch (Exception ex)
            {
                SendMessage(string.Format("An exception has been occurred during the uninstalation.\n\n{0}:\n{1}", ex.Message, ex.StackTrace));
            }
        }

        #region Private Methods
        private void SendMessage(string message)
        { 
            if(OnSendMessage != null) 
                OnSendMessage(this, new AddInInstallMessageEventArgs(message));
        }

        private void RemoveAssembly(string assemblyFullPath)
        {
            File.Delete(assemblyFullPath);

            RemoveReferencedAssemblies(assemblyFullPath);
        }

        private void RemoveReferencedAssemblies(string assemblyFullPath)
        {
            List<string> assemblies = new List<string>();

            Assembly addInAssembly = Assembly.LoadFile(Path.Combine(AddInRegistryResources.GetBaseDirectory(), new FileInfo(assemblyFullPath).Name));

            foreach (AssemblyName assemblyName in addInAssembly.GetReferencedAssemblies())
            {
                Assembly refAssembly = Assembly.Load(assemblyName);

                if (!refAssembly.GlobalAssemblyCache
                    && assemblyName.Name != "AddIn.EA.Core"
                    && assemblyName.Name != "AddIn.EA.Framework"
                    && assemblyName.Name != "EA_Application")
                {
                    string fileToDelete = Path.Combine(AddInRegistryResources.GetApplicationFolder(), new FileInfo(refAssembly.Location).Name);
                    if (File.Exists(fileToDelete))
                        File.Delete(fileToDelete);
                }
            }
        }

        private FileInfo CopyToAddInFolder(string assemblyFullPath)
        {
            FileInfo assemblyFile = new FileInfo(assemblyFullPath);

            if (!assemblyFile.Exists)
            {
                SendMessage(string.Format("Could not find the assembly {0}.", assemblyFullPath));
                return null;
            }

            Assembly addInAssembly = Assembly.LoadFile(assemblyFile.FullName);

            foreach (AssemblyName assemblyName in addInAssembly.GetReferencedAssemblies())
            {
                Assembly refAssembly = Assembly.Load(assemblyName);

                if (!refAssembly.GlobalAssemblyCache
                    && assemblyName.Name != "AddIn.EA.Core"
                    && assemblyName.Name != "AddIn.EA.Framework"
                    && assemblyName.Name != "EA_Application")
                {
                    CopyToAddInFolder(refAssembly.Location);
                }
            }

            string destFile = Path.Combine(AddInRegistryResources.GetApplicationFolder(), assemblyFile.Name);

            return assemblyFile.CopyTo(destFile, true);
        }

        private AddIn ReadAssembly(FileInfo assemblyFile)
        {
            Assembly assembly = Assembly.LoadFile(assemblyFile.FullName);

            AddIn addIn = new AddIn();
            addIn.XmlFileName = string.Format("{0}.xml", assembly.ManifestModule.Name);
            addIn.Body = ReadBody(assembly);
            
            return addIn;
        }

        private AddIn.IGenerateXmlElement[] ReadBody(Assembly assembly)
        { 
            List<Type> types = new List<Type>(assembly.GetTypes());

            string[] menuNames = GetMenuNames(types);

            List<AddIn.IGenerateXmlElement> bodyItems = new List<AddIn.IGenerateXmlElement>();

            if (types.Count > 1 || menuNames.Length > 0)
            {
                if (menuNames.Length > 0)
                {
                    AddIn.IGenerateXmlElement[] menus = GetMenus(menuNames, types);
                    if (menus.Length > 0)
                        bodyItems.AddRange(menus);
                }

                AddIn.IGenerateXmlElement items = GetItems(null, types);
                if(items != null && ((AddIn.Items)items).Count > 0)
                    bodyItems.Add(items);
            }
            else
            {
                AddIn.IGenerateXmlElement item = GetItem(types[0]);
                if(item != null)
                    bodyItems.Add(item);
            }
            
            return bodyItems.ToArray();
        }

        private string[] GetMenuNames(List<Type> types)
        {
            List<string> menus = new List<string>();

            foreach (Type type in types)
            {
                AddInMenuAttribute attrib;
                if (FindMenuAttribute(type, out attrib))
                {
                    if(!String.IsNullOrEmpty(attrib.MenuName) && !menus.Contains(attrib.MenuName))
                        menus.Add(attrib.MenuName);
                }
            }

            return menus.ToArray();
        }

        private AddIn.IGenerateXmlElement[] GetMenus(string[] menuNames, List<Type> types)
        {
            List<AddIn.IGenerateXmlElement> menus = new List<AddIn.IGenerateXmlElement>();

            foreach (string menuName in menuNames)
            {
                AddIn.Menu menu = new AddIn.Menu(menuName);
                menu.Items = (AddIn.Items)GetItems(menuName, types);
                if(menu.Items.Count > 0)
                    menus.Add(menu);
            }

            return menus.ToArray();
        }

        private AddIn.IGenerateXmlElement GetItems(string menuName, List<Type> types)
        { 
            AddIn.Items items = new AddIn.Items();

            foreach (Type type in types)
            {
                AddInMenuAttribute attrib;
                if (FindMenuAttribute(type, out attrib))
                {
                    if (attrib.MenuName == menuName)
                        items.Add((AddIn.Item)GetItem(type));
                }
            }

            return items;
        }

        private AddIn.IGenerateXmlElement GetItem(Type type)
        { 
            AddInMenuAttribute attrib;
            if (FindMenuAttribute(type, out attrib) && type.GetInterface("IAddInArchitecture") != null)
            {
                return new AddIn.Item(attrib.ItemDescription, type.AssemblyQualifiedName);
            }
            else
            {
                return null;
            }
        }

        private bool FindMenuAttribute(Type type, out AddInMenuAttribute attrib)
        {
            object[] attributes = type.GetCustomAttributes(typeof(AddInMenuAttribute), true);
            if (attributes.Length > 0)
            {
                attrib = (AddInMenuAttribute)attributes[0];
                return true;
            }
            else 
            {
                attrib = null;
                return false;
            }
        }
        #endregion

        #endregion

    }
}
